package com.googlecode.java_cl_parser;

import org.apache.commons.io.IOUtils;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;

import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class UsageException extends Exception
{
	private final List<CommonsCLILauncher.Data> availableOptions;
	private Object cliEntry;

	public UsageException(Object entry, List<CommonsCLILauncher.Data> options)
	{
		cliEntry = entry;
		availableOptions = options;
	}

	public UsageException(Object entry, List<CommonsCLILauncher.Data> options, String message)
	{
		super(message);

		cliEntry = entry;
		availableOptions = options;
	}

	public UsageException(Object entry, List<CommonsCLILauncher.Data> options, String message, Throwable cause)
	{
		super(message, cause);

		cliEntry = entry;
		availableOptions = options;
	}

	public UsageException(Object entry, List<CommonsCLILauncher.Data> options, Throwable cause)
	{
		super(cause);

		cliEntry = entry;
		availableOptions = options;
	}

	public List<CommonsCLILauncher.Data> getAvailableOptions()
	{
		return availableOptions;
	}

	public String getFormattedUsageStatement()
	{
		CLIEntry entry = cliEntry.getClass().getAnnotation(CLIEntry.class);

		Map<String, Object> map = new HashMap<String, Object>();

		String value = entry.nickName();
		if (value.equals("\0"))
		{
			value = cliEntry.getClass().getSimpleName();
		}
		map.put("nickName", value);

		map.put("synopsis", StringUtil.wrapTextToList(createSynopsis(value, availableOptions), 80));

		map.put("description", StringUtil.wrapTextToList(entry.description(), 80));
		map.put("version", entry.version());

		value = entry.contact();
		if (!value.equals("\0"))
		{
			map.put("contact", value);
		}

		value = entry.versionControl();
		if (!value.equals("\0"))
		{
			map.put("scm", value);
		}

		value = entry.documentationUrl();
		if (!value.equals("\0"))
		{
			map.put("documentationUrl", value);
		}

		List<OptionLine> optLines = new ArrayList<OptionLine>();

		for (CommonsCLILauncher.Data option : availableOptions)
		{
			OptionLine oline = new OptionLine();

			oline.setShortName("-" + option.clioption.name());
			oline.setLongName("--" + option.clioption.longName());

			String defaultValue = option.clioption.defaultValue();

			if (defaultValue.equals("\0"))
			{
				defaultValue = null;
			}

			String type = null;

			if (option.type != null)
			{
				switch (option.type)
				{

					case bool:
					case Bool:
						type = "boolean";
						break;
					case integer:
					case Integer:
						type = "integer";
						break;
					case long_integer:
					case Long:
						type = "long";
						break;
					case string:
					case String:
					case Object:
						type = "string";
						break;
				}
			}

			oline.setDataType(type);

			oline.setDefaultValue(defaultValue);
			oline.setDescriptionLines(StringUtil.wrapTextToList(option.clioption.description(), 60));

			optLines.add(oline);
		}

		map.put("options", optLines);

		try
		{
			String template = IOUtils.toString(getClass().getResource("/usage_template.vm"));

			VelocityContext context = new VelocityContext(map);

			VelocityEngine velocityEngine = new VelocityEngine();

			velocityEngine.init();

			StringWriter w = new StringWriter();

			velocityEngine.evaluate(context, w, "log", template);

			return w.toString();
		}
		catch (IOException e)
		{
			throw new RuntimeException(e);
		}
	}

	private String createSynopsis(String value, List<CommonsCLILauncher.Data> availableOptions)
	{
		// start with the nick name
		StringBuilder stb = new StringBuilder(value);

		if (availableOptions.size() != 0)
		{
			for (CommonsCLILauncher.Data optionData : availableOptions)
			{
				// leave a blank space before every option so the line can wrap
				stb.append(' ');

				if (!optionData.clioption.required())
				{
					stb.append('[');
				}

				stb.append('-');
				stb.append(optionData.clioption.name());

				if (optionData.clioption.valueType() != CLIOption.value_type.not_allowed)
				{
					describeArguments(stb, optionData);
				}

				if (!optionData.clioption.required())
				{
					stb.append(']');
				}
			}
		}

		return stb.toString();
	}

	private void describeArguments(StringBuilder stb, CommonsCLILauncher.Data optionData)
	{
		stb.append(' ');

		if (optionData.clioption.valueType() == CLIOption.value_type.required)
		{
			stb.append('<');
		}
		else
		{
			stb.append('(');
		}

		describeOneArgument(stb, optionData);

		if (optionData.clioption.valueCardinality() != 1)
		{
			stb.append('(');
			stb.append(optionData.clioption.valueSeparator());
			describeOneArgument(stb, optionData);
			stb.append("){");

			if (optionData.clioption.valueCardinality() == 0)
			{
				stb.append('N');
			}
			else
			{
				stb.append(optionData.clioption.valueCardinality() - 1);
			}

			stb.append('}');
		}

		if (optionData.clioption.valueType() == CLIOption.value_type.required)
		{
			stb.append('>');
		}
		else
		{
			stb.append(')');
		}

		if (!optionData.clioption.defaultValue().equals("\0"))
		{
			stb.append(" default \"");
			stb.append(optionData.clioption.defaultValue());
			stb.append('\"');
		}
	}

	private void describeOneArgument(StringBuilder stb, CommonsCLILauncher.Data optionData)
	{
		boolean notEnum = optionData.clioption.enumeratedValues().length == 1 && optionData.clioption.enumeratedValues()[0].equals("\0");

		if (notEnum)
		{
			switch (optionData.type)
			{
				case bool:
				case Bool:
					stb.append("boolean");
					break;
				case integer:
				case Integer:
					stb.append("integer");
					break;
				case long_integer:
				case Long:
					stb.append("long");
					break;
				case string:
				case String:
				case Object:
					stb.append("string");
					break;
			}
		}
		else
		{
			// process an enum list
			boolean first = true;

			for (String enu : optionData.clioption.enumeratedValues())
			{
				if (first)
				{
					first = false;
				}
				else
				{
					stb.append('|');
				}
				stb.append(enu);
			}
		}
	}
}